CSRF攻击防范

        CSRF攻击必须依次完成以下两个条件:

  1. 登录受信任网站A,并在本地生成Cookie。
  2. 在不登出A的情况下,访问危险网站B。

        关闭浏览器不能结束一个会话,但大多数人都会错误的认为关闭浏览器就等于退出登录/结束会话了

        CSRF攻击形成的原因:

  1. 网站违反了HTTP协议,使用GET请求更新资源(非常危险).
  2. 使用无校验的表单

        CSRF攻击是源于WEB的隐式身份验证机制!(比如你在A网站登录后,在同一个浏览器的tab页打开B网站网页, 而B网站网页有一些脚本链接到A网站并偷偷的发送请求更新你账号的信息.如果没有防CSRF攻击,这样是可以实现的.)

        WEB的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的!

        CSRF防范方式:客户端页面增加伪随机数。每次需要修改该用户的数据库信息时必须带上该随机数,否则不受理.

        解决办法: 在Form表单加一个hidden field,里面是服务端生成的足够随机数的一个Token(恶意网站猜不到也无法获取到相同的Token), 然后使用一个拦截器interceptor来检查每一个非get请求, 看该token与服务器token是否一致,不一致的不受理该请求.

        以下是token 的工具管理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.xxx.xxx.util;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.UUID;
/**
* CSRFtoken管理类
*
* @author 作者 yss
* @version 版本号 v1.0
*/
public final class CSRFTokenManager {
/**
* 约定规范:表单提交时的token的input的name属性必须为该值才能获取到后台返回的token。
* 下面是使用EL表达式接收从后台返回的token参数
* 如: <input type="hidden" id="CSRFToken" value="${CSRFToken}">
*/
public static final String CSRF_PARAM_NAME = "CSRFToken";
/**
* 存放在session中的token名称(跟上面的name属性值不一定一样)
*/
public static final String CSRF_TOKEN_FOR_SESSION_ATTR_NAME = CSRFTokenManager.class.getName() + ".tokenval";
/**
*从session中获取token字符串
* @param session
* @return
*/
public static String getTokenForSession(HttpSession session) {
String token = null;
//保证session只存在一个token,避免多线程情况下产生冲突。
synchronized (session) {
token = (String) session.getAttribute(CSRF_TOKEN_FOR_SESSION_ATTR_NAME);//尝试获取session中的token
if (null == token) {//如果session中没有token,就重新生成一个token
token = UUID.randomUUID().toString();
session.setAttribute(CSRF_TOKEN_FOR_SESSION_ATTR_NAME, token);
}
}
return token;
}
/**
*获取到request中的token值。
* @param request
* @return
*/
public static String getTokenFromRequest(HttpServletRequest request) {
return request.getParameter(CSRF_PARAM_NAME);
}
//构造器
private CSRFTokenManager() {
}
}

        后台返回token参数给页面:

        String token = CSRFTokenManager.getTokenForSession(request.getSession());//uuid生成的随机token

        modelAndView.addObject(CSRFTokenManager.CSRF_PARAM_NAME, token);//添加token参数 <%–token–%>

        编写代码要符合HTTP规范,不要使用GET请求更新资源

        在非GET请求中带上token 参数(ajax请求也要),然后使用拦截器检查.

        对于受到CSRF攻击使用的是抛自定义异常,然后使用springmvc全局异常进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.xxx.xxx.interceptor;
import com.xxx.xxx.exception.CSRFException;
import com.xxx.xxx.util.CSRFTokenManager;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 对于未登录用户的请求进行拦截,确保用户已经登录,然后进行后续的网页请求
*
* @Author yss
* @Version 1.0
* @see
*/
public class CSRFInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// Enumeration parasm = request.getParameterNames();
if (!"GET".equals(request.getMethod())) {//非get请求
String CSRFToken = CSRFTokenManager.getTokenFromRequest(request);//页面传过来的csrf参数
if (CSRFToken == null || !CSRFToken.equals(CSRFTokenManager.getTokenForSession(request.getSession()))) {//token不对应
throw new CSRFException("CSRF攻击");//抛异常后将会进入springmvc全局异常处理体系
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
super.postHandle(request, response, handler, modelAndView);
}
}

        在spring的配置文件中添加拦截器使其生效

1
2
3
4
5
6
7
<mvc:interceptors>
<!-- 防止CSRF攻击的拦截器 -->
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean id="CSRFInterceptor" class="com.xxx.xxx.interceptor.CSRFInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>